//***************************************************************************************
//
//! \file ftp_client.c
//! Code for ftp client, basic connection to a local ftp server, connection host
//! ip:127.0.0.1, default port:21.
//!
//! \author    ssr
//! \version   V1.0
//! \date      2022-11-6
//! \copyright GNU Public License V3.0
//
//***************************************************************************************

// #ifndef FTP_CLIENT
// #define FTP_CLIENT
#include "ftp_client.h"
#include "cmd_client.h"
#include "config.h"
#include <arpa/inet.h>
#include <time.h>
// #include <cstddef>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>

#define DEFAULT_FTP_PORT 21
#define RCV_BUF_MAX_LEN 510

struct sockaddr_in ftp_server, local_host;
struct hostent *server_hostent;
static struct termios stored_settings;

int sock_control;
char user[64];   //用户名
char passwd[64]; //密码
int mode = PORT;

/// @brief 打印报错信息并退出。
/// @param err_msg
/// @param err_code
void cmd_err_exit(char *err_msg, int err_code)
{
  printf("%s\n", err_msg);
  exit(err_code);
}
/// @brief 关闭显示输入字符（用于输入密码）
void echo_off(void)
{
  struct termios new_settings;
  tcgetattr(0, &stored_settings);
  new_settings = stored_settings;
  new_settings.c_lflag &= (~ECHO);
  tcsetattr(0, TCSANOW, &new_settings);
  return;
}
/// @brief 打开显示输入字符
void echo_on(void)
{
  tcsetattr(0, TCSANOW, &stored_settings);
  return;
}
/// @brief 接受来自服务端的回应。
/// @param sock_fd 连接服务端的套接字。
/// @return 若输入为空，返回0；否则返回对应的reply_code数字。
int ftp_get_reply(int sock_fd)
{
  static int reply_code = 0, count = 0;
  char rcv_buf[RCV_BUF_MAX_LEN + 2];
  count = read(sock_fd, rcv_buf, RCV_BUF_MAX_LEN);
  if (count > 0)
  {
    reply_code = atoi(rcv_buf);
  }
  else
  {
    return 0;
  }
  char* out = rcv_buf;
  if(reply_code > 0) out += 4;
  while (1)
  {
    if (count <= 0)
    {
      break;
    }
    rcv_buf[count] = '\0';
    char *tmp = rcv_buf + strlen(rcv_buf) - 9;
    if (strcmp(tmp, "task done") == 0)
    {
      *tmp = '\0';
      printf("%s", out);
      break;
    }
    printf("%s", out);
    count = read(sock_fd, rcv_buf, 510);
    out = rcv_buf;
  }
  return reply_code;
}
/// @brief 拼接给出的两个参数后向客户端发送。
/// @return 成功则返回发送的字符数，否则返回-1。
int ftp_send_cmd(const char *s1, const char *s2, int sock_fd)
{
  char send_buf[256];
  int send_err, len;

  if (s1)
  {
    strcpy(send_buf, s1);

    if (s2)
    {
      strcat(send_buf, s2);
      strcat(send_buf, "\0");
      len = strlen(send_buf);
      send_err = send(sock_fd, send_buf, len, 0);
    }
    else
    {
      strcat(send_buf, "\0");
      len = strlen(send_buf);
      send_err = send(sock_fd, send_buf, len, 0);
    }
  }

  if (send_err < 0)
  {
    printf("Send() error!\n");
  }
  return send_err;
}
//! @brief 连接到服务器。
//! @param s_addr socket连接的地址
//! @return 成功则返回所用套接字的描述符，否则返回-1。
int xconnect(struct sockaddr_in *s_addr)
{
  struct timeval outtime;
  outtime.tv_sec = 3;
  outtime.tv_usec = 0;
  int set;
  int s = socket(AF_INET, SOCK_STREAM, 0); // 创建socket，不同系统、进程间有保障连接：tcp协议
  if (s < 0)
  {
    cmd_err_exit("Create socket error!", 249);
  }
  set = setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &outtime,
                   sizeof(outtime)); //< 套接字级别设置超时时间
  if (set != 0)
  {
    printf("Set socket %s errno:%d\n", strerror(errno), errno);
    cmd_err_exit("set socket", 1);
  }

  //连接到服务器，如果连接成功，connect函数返回0，如果不成功，返回-1。
  if (connect(s, (struct sockaddr *)s_addr, sizeof(struct sockaddr_in)) < 0)
  {
    printf("Can't connect to server %s, prot %d\n", inet_ntoa(s_addr->sin_addr),
           ntohs(ftp_server.sin_port));
    exit(252);
  }
  return s;
}
/// @brief PORT模式下，随机生成一个端口号。
/// @return 生成的端口号
int rand_local_port()
{
  int local_port;

  srand((unsigned)time(NULL));
  local_port = rand() % 10000 + 1025;
  return local_port;
}
//! @brief pasv模式下，向server发送PASV指令，接收server发回的端口号（第一次握手
//! @return 若执行成功，返回server打开的端口号；否则返回0。
int get_port()
{
  char port_respond[512];
  char *buf_ptr;
  int count, port_num;

  ftp_send_cmd("PASV", NULL, sock_control);

  count = read(sock_control, port_respond, 510);

  if (count <= 0)
  {
    return 0;
  }

  port_respond[count] = '\0';
  if (atoi(port_respond) == 227)
  {
    buf_ptr = strrchr(port_respond, ',');
    port_num = atoi(buf_ptr + 1);

    *buf_ptr = '\0';

    buf_ptr = strrchr(port_respond, ',');
    port_num += atoi(buf_ptr + 1) * 256;
    return port_num;
  }

  return 0;
}
//! @brief 建立数据连接，被动模式下，从server获得端口号，通过server打开的这个端口与server建立数据连接;主动模式下，client指定本地端口号后，向server发PORT指令，然后建立一个新的数据连接。
//! @param msg 此函数负责填充msg结构体中的ftp_data_sock
void xconnect_ftpdata(Msg *msg)
{
  // PASV
  if (mode == PASV)
  {
    int data_port = get_port();
    if (data_port != 0)
      ftp_server.sin_port = htons(data_port);

    msg->ftp_data_sock = xconnect(&ftp_server);
    ftp_get_reply(msg->ftp_control_sock);
  }
  // PORT
  else
  {
    int client_port, get_sock, opt, set;
    char cmd_buf[32];
    struct timeval outtime;
    struct sockaddr_in local;

    char local_ip[24];
    char *ip_1, *ip_2, *ip_3, *ip_4;
    int addr_len = sizeof(struct sockaddr);
    // 指定客户端使用的端口号，端口号随机产生
    client_port = rand_local_port();
    get_sock = socket(AF_INET, SOCK_STREAM, 0);

    if (get_sock < 0)
    {
      cmd_err_exit("socket()", 1);
    }

    outtime.tv_sec = 7;
    outtime.tv_usec = 0;
    opt = SO_REUSEADDR;
    set = setsockopt(get_sock, SOL_SOCKET, SO_RCVTIMEO, &outtime,
                     sizeof(outtime));
    if (set != 0)
    {
      printf("Set socket %s errno:%d\n", strerror(errno), errno);
      cmd_err_exit("set socket", 1);
    }

    // 设置端口释放后立即就可以被再次使用。
    set = setsockopt(get_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    if (set != 0)
    {
      printf("set socket %s errno:%d\n", strerror(errno), errno);
      cmd_err_exit("set socket", 1);
    }

    // 填充本地地址

    bzero(&local_host, sizeof(local_host));

    local_host.sin_family = AF_INET;
    local_host.sin_port = htons(client_port);

    local_host.sin_addr.s_addr = htonl(INADDR_ANY);

    bzero(&local, sizeof(struct sockaddr));

    while (1)
    {
      // 将本地地址提供给socket
      set = bind(get_sock, (struct sockaddr *)&local_host, sizeof(local_host));
      if (set != 0 && errno == 11)
      {
        client_port = rand_local_port();
        continue;
      }
      // 接收来自server的连接
      set = listen(get_sock, 1);
      if (set != 0 && errno == 11)
      {
        cmd_err_exit("listen()", 1);
      }

      //获取ip地址
      if (getsockname(sock_control, (struct sockaddr *)&local,
                      (socklen_t *)&addr_len) < 0)
        return;

      snprintf(local_ip, sizeof(local_ip), "%s", inet_ntoa(local.sin_addr));
      local_ip[strlen(local_ip)] = '\0';
      ip_1 = local_ip;
      ip_2 = strchr(local_ip, '.');
      *ip_2 = '\0';
      ip_2++;
      ip_3 = strchr(ip_2, '.');
      *ip_3 = '\0';
      ip_3++;
      ip_4 = strchr(ip_3, '.');
      *ip_4 = '\0';
      ip_4++;

      snprintf(cmd_buf, sizeof(cmd_buf), "PORT %s, %s, %s, %s, %d, %d", ip_1,
               ip_2, ip_3, ip_4, client_port >> 8, client_port & 0xff);
      ftp_send_cmd(cmd_buf, NULL, sock_control);

      if (ftp_get_reply(sock_control) != 200)
      {
        printf("Can not use PORT mode! Please use \"mode\" change to PASV "
               "mode.\n");
        cmd_err_exit("socket()", 1);
      }
      else
      {
        int new_sock;
        int set = sizeof(local_host);
        int i = 0;
        while (i < 3)
        {
          new_sock = accept(get_sock, (struct sockaddr *)&local_host,
                            (socklen_t *)&set);
          // 等待连接出错时，允许第二第三次尝试
          if (new_sock == -1)
          {
            printf("accept return:%s errno:%d\n", strerror(errno), errno);
            i++;
            continue;
          }
          else
            break;
        }
        if (new_sock == -1)
        {
          printf("Sorry, you can not use PORT mode, There is someting wrong "
                 "when the server connect to you.\n");
          cmd_err_exit("socket()", 1);
        }
        msg->ftp_data_sock = new_sock;
        break;
      }
    }
  }
}
/// @brief 获取用户名。
void get_user()
{
  char read_buf[64];
  printf("User:");
  fgets(read_buf, sizeof(read_buf), stdin);
  strncpy(user, read_buf, strlen(read_buf) - 1);
}
/// @brief 获取密码。
void get_pass()
{
  char read_buf[64];

  printf("Password: ");
  echo_off(); //可以改成星号maybe

  fgets(read_buf, sizeof(read_buf), stdin);
  strncpy(passwd, read_buf, strlen(read_buf) - 1);

  echo_on();
  printf("\n");
}
/// @brief 客户端登录交互。
/// @return 登录成功则返回1,否则返回0。
int ftp_login()
{
  int code;

  get_user();

  if (ftp_send_cmd("USER ", user, sock_control) < 0)
    cmd_err_exit("Can not send message", 1);

  code = ftp_get_reply(sock_control);

  // 331:User name okay, need password.
  if (code == 331)
  {
    get_pass();

    if (ftp_send_cmd("PASS ", passwd, sock_control) <= 0)
      cmd_err_exit("Can not send message", 1);

    else
      code = ftp_get_reply(sock_control);

    // 230:User logged in, proceed.
    if (code == 230)
      return 1;
    else
    {
      printf("Password error!\n");
      return 0;
    }
  }

  else
  {
    printf("User error!\n");
    return 0;
  }
}
/// @brief 设置客户端的sockaddr_in。
/// @return 成功则返回1,失败则返回错误码。
int fill_host_addr(char *host_ip_addr, struct sockaddr_in *host, int port)
{
  if (port <= 0 || port > 65535)
  {
    return 254;
  }
  //初始化
  bzero(host, sizeof(struct sockaddr_in));
  // TCP/IP
  host->sin_family = AF_INET;

  if (inet_addr(host_ip_addr) != -1)
  {
    host->sin_addr.s_addr = inet_addr(host_ip_addr);
  }
  else
  {
    return 253;
  }
  host->sin_port = htons(port);
  return 1;
}
//! @brief 格式化指令,对错误指令返回ERROR flag
//! @param user_cmd 用户输入的指令
//! @return cmd_flag，用于指示调用何种功能函数完成指令
int ftp_user_cmd(char *user_cmd)
{

  char *cmd = strtok(user_cmd, " ");

  if (strlen(cmd) == 2 && !strncmp(user_cmd, "ls", 2))
    return LS;
  if (strlen(cmd) == 3 && !strncmp(user_cmd, "pwd", 3))
    return PWD;
  if (strlen(cmd) == 2 && !strncmp(user_cmd, "cd", 2))
    return CD;
  if (strlen(cmd) == 3 && !strncmp(user_cmd, "put", 3)){
    if (!strncmp(user, "anonymous", 8))
    {
      return NO_AUTH;
    }
    return PUT;
  } 
  if (strlen(cmd) == 3 && !strncmp(user_cmd, "get", 3)){
    if (!strncmp(user, "anonymous", 8))
    {
      return NO_AUTH;
    }
    return GET;
  }
  if (strlen(cmd) == 4 && !strncmp(user_cmd, "quit", 4))
    return QUIT;
  if (strlen(cmd) == 4 && !strncmp(user_cmd, "mode", 4))
    return CHMOD;
  if (strlen(cmd) == 3 && !strncmp(user_cmd, "lls", 3))
    return LLS;
  if (strlen(cmd) == 4 && !strncmp(user_cmd, "lpwd", 4))
    return LPWD;
  if (strlen(cmd) == 3 && !strncmp(user_cmd, "lcd", 3))
    return LCD;
  if (strlen(cmd) == 5 && !strncmp(user_cmd, "mkdir", 5))
    return MKDIR;
  if (strlen(cmd) == 5 && !strncmp(user_cmd, "rmdir", 5))
    return RMDIR;
  if (strlen(cmd) == 6 && !strncmp(user_cmd, "delete", 6))
    return DELETE;
  if (strlen(cmd) == 4 && !strncmp(user_cmd, "help", 4))
    return HELP;

  return ERROR;
}
/// @brief 处理启动之后与用户的交互。
int start_ftp_cmd(char *host_ip_addr, int port)
{
  int err;
  int cmd_flag;
  char user_cmd[1024];

  err = fill_host_addr(host_ip_addr, &ftp_server, port);

  if (err == 254)
    cmd_err_exit("Invalid port!", 254);
  if (err == 253)
    cmd_err_exit("Invalid server address!", 253);

  sock_control = xconnect(&ftp_server);

  // 220:Service ready for new user.
  if ((err = ftp_get_reply(sock_control)) != 220)
    cmd_err_exit("Connect error!", 220);

  do
  {
    err = ftp_login();
  } while (err != 1);

  Msg msg;
  msg.file_type = BINARY;
  msg.mode = mode;
  msg.ftp_control_sock = sock_control;
  msg.ftp_data_sock = 0;
  strcpy(msg.control_info, "");

  while (1)
  {
    printf("ftp_cli>");

    fgets(user_cmd, 510, stdin);

    fflush(stdin);

    if (user_cmd[0] == '\n')
      continue;

    for (int i = strlen(user_cmd) - 1; i >= 0; i--)
    {
      if (user_cmd[i] == '\n' || user_cmd == ' ')
      {
        user_cmd[i] = '\0';
      }
      else
        break;
    }

    if (strlen(user_cmd) <= 0)
      continue;

    strcpy(msg.control_info, user_cmd);
    cmd_flag = ftp_user_cmd(user_cmd);

    switch (cmd_flag)
    {
    case LS:
      xconnect_ftpdata(&msg);
      ftp_list(&msg);
      break;
    case PWD:
      ftp_pwd(&msg);
      break;
    case CD:
      ftp_cd(&msg);
      break;
    case PUT:
      xconnect_ftpdata(&msg);
      ftp_put(&msg);
      break;
    case GET:
      xconnect_ftpdata(&msg);
      ftp_get(&msg);
      break;
    case MKDIR:
      ftp_mkdir(&msg);
      break;
    case RMDIR:
      ftp_rmdir(&msg);
      break;
    case DELETE:
      ftp_delete(&msg);
      break;
    case QUIT:
      ftp_quit(&msg);
      printf("BYE TO myFTP!\n");
      exit(0);
    case LPWD:
      local_pwd();
      break;
    case LLS:
      local_list();
      break;
    case LCD:
      local_cd(user_cmd);
      break;
    case CHMOD:
      mode = (mode + 1) % 2;
      msg.mode = mode;
      if (mode == PASV)
        printf("change mode to PASV\n");
      else
        printf("change mode to PORT\n");
      break;
    case HELP:
      show_help();
      break;
    case NO_AUTH:
      printf("Permission denied.Anonymous user has no permmission.\n");
      show_help();
      memset(user_cmd, '\0', sizeof(user_cmd));
      break;
    default:
      printf("command error\n");
      show_help();
      break;
    }
    memset(user_cmd, '\0', sizeof(user_cmd));
  }
  return 1;
}
/// @brief 启动客户端，要求ip地址参数。默认在端口21启动。
int main(int argc, char *argv[])
{
  if (argc == 1)
  {
    printf("please enter host ip\n");
  }
  else
  {
    if (argc == 2)
    {
      start_ftp_cmd(argv[1], DEFAULT_FTP_PORT);
    }
    else
    {
      printf("Input unavailable!\n");
      exit(-1);
    }
  }
  return 1;
}
